package cn.itcast.client;

import cn.itcast.message.*;
import cn.itcast.protocol.MessageCodecSharable;
import cn.itcast.protocol.ProcotolFrameDecoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.nio.file.Watchable;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;


/**
 *
 *   我们的业务就是 聊天室 然后客户端得先登录
 *   其实netty最重要的 1.byteBuf 就是一个缓冲区  2.channel 就是全双工的是数据交换的通道
 *   3. 各种handler是个责任链形式的用来表明入站的时候我要做什么 出站的时候我要做什么 所以很多协议编解码的这种公共的肯定就是客户端服务端都要的
 *   然后日志的这种肯定也是都要的 然后剩下的处理器就主要用来处理业务的  我这边遇到数据来的时候我要做什么业务 另一边我要做什么业务这种
 *   所以我们的业务核心就是写handler
 *   4.然后netty还需要理解的就是eventLoop对象 还有里面的异步 同步 阻塞这些 还有消息的传递(怎么触发消息传递等这种问题)
 *
 */
@Slf4j
public class ChatClient {
    public static void main(String[] args) {
        NioEventLoopGroup group = new NioEventLoopGroup();
        LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG);
        MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();
        //https://blog.csdn.net/qq_36248805/article/details/109644890 CountDownLatch详解
        CountDownLatch WAIT_FOR_LOGIN = new CountDownLatch(1);  //默认计数器是1
        AtomicBoolean LOGIN = new AtomicBoolean(false); //是否登录成功标识

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(group);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() { //这里是启动的一个新的线程去做的
                @Override
                protected void initChannel(SocketChannel ch) throws Exception { //initChannel() 遇到accept()后触发(建立连接) 然后去装载处理器
                    ch.pipeline().addLast(new ProcotolFrameDecoder()); //处理半包闭包问题的处理器  用我们自定义的处理器
                    //ch.pipeline().addLast(LOGGING_HANDLER);
                    ch.pipeline().addLast(MESSAGE_CODEC);
                    //服务器端5s没接收到数据就会直接假死了但是其实这样并不是很友好的 可能用户可能只是去喝了杯水 回来连接就断了
                    //所以我们可以做一个心跳来做通讯 心跳的时长要比服务端的时间短
                    //下面的配置: 3s内如果没有想服务器写数据 会触发IdleStateHandler内的事件(IdleState#WRITER_IDLE事件)
                    ch.pipeline().addLast(new IdleStateHandler(0,3,0));
                    //ChannelDuplexHandler 是出站处理器 也是 入站处理器
                    ch.pipeline().addLast(new ChannelDuplexHandler(){
                        //用户事件触发器
                        @Override
                        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
                            IdleStateEvent event = (IdleStateEvent) evt;
                            //处理上面说的 假死写 事件
                            //那么如果是真的假死呢?如果真的假死的话网络卡的话客户端的心跳包都发不出去那么等到5s的时候服务端不就触发了那个5s的读事件了嘛
                            //这样就可以真假死跟假假死的问题都处理了
                            if(event.state() == IdleState.WRITER_IDLE){
                                //log.debug("已经3s没写数据了 该发个心跳了");
                                //心跳只要你们自己服务器端客户端能识别就行了发啥都行
                                //在这里我们就用ping pang来当心跳跟心跳响应  那边没有具体的业务处理ping这个请求的但是能让服务端那边的5s的检测不会触发
                                ctx.writeAndFlush(new PingMessage());
                            }
                        }
                    });
                    //添加一个自定义处理器来处理客户端给服务器端发起登录请求开始聊天 其实就是开启一个自定义处理器来处理业务
                    ch.pipeline().addLast("client handler", new ChannelInboundHandlerAdapter() {
                        //在确保连接建立后会触发active事件 所以这个事件里面就可以去做登录请求
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            //我们可以开个新的线程去做 不然用的就是NIO的这个线程了
                            new Thread(()->{
                                //负责接收用户在控制台的输入,负责向服务器发送消息
                                Scanner scanner = new Scanner(System.in);
                                System.out.println("请输入用户名");
                                String username = scanner.nextLine();
                                System.out.println("请输入密码");
                                String password = scanner.nextLine();
                                //构造登录请求对象
                                LoginRequestMessage message = new LoginRequestMessage(username, password);
                                ctx.writeAndFlush(message); //因为这是一个入站处理器 然后我们写了点数据 因为有写数据所以就会触发出站处理器了 就会经过MESSAGE_CODEC去编码等然后继续下个出站处理器等....
                                //所以消息已经往下传递了所以我们这里可以不用写那行需要把消息往下传递的代码
                                //ctx的写操作是会从当前位置往前去找出站处理器  而ch的写操作是从tail往前去找出站处理器 就是先找MESSAGE_CODEC再找LOGGING_HANDLER...
                                System.out.println("等待后续操作");
                                try {
                                    WAIT_FOR_LOGIN.await();  //当前线程阻塞住 只有当计数器变成0的时候当前线程才会被唤醒
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                                //如果登录失败 线程直接终止 方法也结束了
                                if(!LOGIN.get()){
                                    ctx.channel().close();
                                    return;
                                }
                                //开启后续操作
                                while (true){
                                    //打印功能菜单 其实我们自己的业务
                                    System.out.println("==================================");
                                    System.out.println("send [username] [content]"); //发信息给某人 内容是啥
                                    System.out.println("gsend [group name] [content]"); //发信息给某群
                                    System.out.println("gcreate [group name] [m1,m2,m3...]"); //创建群 群名称 群员
                                    System.out.println("gmembers [group name]"); //找人
                                    System.out.println("gjoin [group name]");  //加群
                                    System.out.println("gquit [group name]");  //退出群
                                    System.out.println("quit");  //退出系统
                                    System.out.println("==================================");

                                    String command = scanner.nextLine(); //接受命令然后我们下面开始做具体业务 不深究
                                    String[] s = command.split(" ");
                                    switch (s[0]){
                                        case "send":
                                            ctx.writeAndFlush(new ChatRequestMessage(username, s[1], s[2]));
                                            break;
                                        case "gsend":
                                            ctx.writeAndFlush(new GroupChatRequestMessage(username, s[1], s[2]));
                                            break;
                                        case "gcreate":
                                            Set<String> set = new HashSet<>(Arrays.asList(s[2].split(",")));
                                            set.add(username); // 加入自己
                                            ctx.writeAndFlush(new GroupCreateRequestMessage(s[1], set));
                                            break;
                                        case "gmembers":
                                            ctx.writeAndFlush(new GroupMembersRequestMessage(s[1]));
                                            break;
                                        case "gjoin":
                                            ctx.writeAndFlush(new GroupJoinRequestMessage(username, s[1]));
                                            break;
                                        case "gquit":
                                            ctx.writeAndFlush(new GroupQuitRequestMessage(username, s[1]));
                                            break;
                                        case "quit":
                                            //客户端关闭也是会发送事件给到服务器端的 有正常退出跟异常退出两种情况
                                            //这里的ctx.channel().close();就是正常断开请求 会触发处理器的inactive()方法
                                            //如果是非正常退出比如我们直接点掉控制台的红色正方形这种 就会触发处理器的exceptionCaught()方法
                                            ctx.channel().close();
                                            return;
                                    }
                                }
                            },"system in").start();
                        }

                        //读到数据的时候会执行的方法
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            log.debug("接收到服务端返回的数据:{}",msg);
                            if(msg instanceof LoginResponseMessage){ //如果是登录响应 代表用户刚刚执行了登录
                                LoginResponseMessage responseMessage = (LoginResponseMessage) msg;
                                if (responseMessage.isSuccess()) {
                                    LOGIN.set(true);
                                }
                                WAIT_FOR_LOGIN.countDown(); //让计数器减1
                            }
                        }
                    });
                }
            });
            Channel channel = bootstrap.connect("localhost", 8080).sync().channel(); //等待与服务器建立完连接才会往下执行
            //我们也可以在这之后去做登录信息 上面的channel是同步获取得到的所以肯定就是连接之后了
            channel.closeFuture().sync();



        } catch (Exception e) {
            log.error("client error", e);
        } finally {
            group.shutdownGracefully();
        }
    }
}
